// THIS IS MAGIC DO NOT TOUCH

#include <input/input.h>
#include <base/interface.h>

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <vector>

HWND _window;

LPDIRECTINPUT8 _pDI;
std::vector<LPDIRECTINPUTDEVICE8> _pJoysticks;
LPDIRECTINPUTDEVICE8 _pKeyboard; //a pointer to the keyboard device
LPDIRECTINPUTDEVICE8 _pMouse;    //a pointer to the mouse device

DIDEVICEOBJECTDATA * _mouseData; //mouse data buffer
DIDEVICEOBJECTDATA * _keyData;
std::vector<DIDEVICEOBJECTDATA *> _joyData;
DIDEVICEOBJECTDATA * _all_data;
char * _data_type;

unsigned long mouse_elements;
unsigned long current_mouse_element;
unsigned long key_elements;
unsigned long current_key_element;
std::vector<unsigned long> joy_elements;
std::vector<unsigned long> current_joy_element;

unsigned long _buffer_size = 64;

BOOL CALLBACK EnumJoysticksCallback(const DIDEVICEINSTANCE* pdid, VOID* pContext);
BOOL CALLBACK EnumObjectCallback(const DIDEVICEOBJECTINSTANCE *pdido, VOID *pContext);

long InitInput(HWND m_hWnd) {

  long hr;

  //create direct input object
  _window = m_hWnd;
  DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&_pDI, NULL);
  
  DIPROPDWORD dipdw;
  dipdw.diph.dwSize       = sizeof(DIPROPDWORD);
  dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER);
  dipdw.diph.dwObj        = 0;
  dipdw.diph.dwHow        = DIPH_DEVICE;
  dipdw.dwData            = _buffer_size;

  DIPROPDWORD dipd_deadzone;
	dipd_deadzone.diph.dwSize = sizeof(DIPROPDWORD);
	dipd_deadzone.diph.dwHeaderSize = sizeof(DIPROPHEADER);
	dipd_deadzone.diph.dwHow = 0;
	dipd_deadzone.diph.dwObj = DIPH_DEVICE;
  dipd_deadzone.dwData = 1000;

  //create mouse and keyboard devices
  _pDI->CreateDevice(GUID_SysKeyboard, &_pKeyboard, NULL);
	_pKeyboard->SetDataFormat(&c_dfDIKeyboard);
	_pKeyboard->SetCooperativeLevel(_window, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);
  _pKeyboard->SetProperty(DIPROP_BUFFERSIZE, &(dipdw.diph));

	_pDI->CreateDevice(GUID_SysMouse, &_pMouse, NULL);
	_pMouse->SetDataFormat(&c_dfDIMouse);
	_pMouse->SetCooperativeLevel(_window, DISCL_FOREGROUND | DISCL_EXCLUSIVE);
  _pMouse->SetProperty(DIPROP_BUFFERSIZE, &(dipdw.diph));

  _pKeyboard->Acquire();
	_pMouse->Acquire();

  //create joystick devices
	if(FAILED(hr = _pDI->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumJoysticksCallback, NULL, DIEDFL_ATTACHEDONLY)))
		return hr;
	for(unsigned int i = 0; i < _pJoysticks.size(); i++)
	{
		if(FAILED(hr = _pJoysticks[i]->SetDataFormat(&c_dfDIJoystick)))
			return hr;
		if(FAILED(hr = _pJoysticks[i]->SetCooperativeLevel(_window, DISCL_FOREGROUND | DISCL_EXCLUSIVE)))
			return hr;
		if(FAILED(hr = _pJoysticks[i]->EnumObjects(EnumObjectCallback, (VOID*)i, DIDFT_ALL)))
			return hr;
    if(FAILED(hr = _pJoysticks[i]->SetProperty(DIPROP_BUFFERSIZE, &dipdw.diph)))
	    return hr;
    if(FAILED(hr = _pJoysticks[i]->SetProperty(DIPROP_DEADZONE, &dipd_deadzone.diph)))
	    return hr;

    _pJoysticks[i]->Acquire();
	}

  _mouseData = new DIDEVICEOBJECTDATA[_buffer_size];
  _keyData = new DIDEVICEOBJECTDATA[_buffer_size];
  _joyData.resize(_pJoysticks.size());
  for(unsigned int i = 0; i < _joyData.size(); i++) {
    _joyData[i] = new DIDEVICEOBJECTDATA[_buffer_size];
  }
  _all_data = new DIDEVICEOBJECTDATA[(_pJoysticks.size() + 2) * _buffer_size];
  _data_type = new char[(_pJoysticks.size() + 2) * _buffer_size];

  joy_elements.resize(_pJoysticks.size());
  current_joy_element.resize(_pJoysticks.size());

  return 0;
}

BOOL CALLBACK EnumJoysticksCallback(const DIDEVICEINSTANCE* pdid, VOID* pContext) {
  _pJoysticks.push_back(NULL);

	if(FAILED(_pDI->CreateDevice(pdid->guidInstance, &(_pJoysticks.back()), NULL)))
	    return DIENUM_STOP;

  return DIENUM_CONTINUE;
}

BOOL CALLBACK EnumObjectCallback(const DIDEVICEOBJECTINSTANCE *pdido, VOID *pContext) {
	if(pdido->dwType & DIDFT_AXIS)
	{
		DIPROPRANGE dipr;
		dipr.diph.dwSize = sizeof(DIPROPRANGE);
		dipr.diph.dwHeaderSize = sizeof(DIPROPHEADER);
		dipr.diph.dwHow = DIPH_BYID;
		dipr.diph.dwObj = pdido->dwType;
		dipr.lMin = -1000;
		dipr.lMax = +1000;

		if(FAILED(_pJoysticks[(int)pContext]->SetProperty(DIPROP_RANGE, &dipr.diph)))
			return DIENUM_STOP;
	}
  
	return DIENUM_CONTINUE;
}

long DestroyInput() {
  delete(_data_type);
  delete(_all_data);
  for(unsigned int i = 0; i < _joyData.size(); i++) {
    delete(_joyData[i]);
  }
  delete(_mouseData);
  delete(_keyData);

  for(unsigned int i = 0; i < _pJoysticks.size(); i++) {
    _pJoysticks[i]->Release();
  }
  _pMouse->Release();
  _pKeyboard->Release();
  _pDI->Release();

  return 0;
}

long GetInputData() {

  HRESULT hr;
  long retval = 0;

  mouse_elements = _buffer_size;
  current_mouse_element = 0;
  key_elements = _buffer_size;
  current_key_element = 0;
  
  //mouse
  hr = _pMouse->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), _mouseData, &mouse_elements, 0);
  if(hr != DI_OK) {//mouse was lost
      hr = _pMouse->Acquire();
      if(hr == DI_OK)
        _pMouse->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), _mouseData, &mouse_elements, 0);
      else
        retval = -1;
  }

  //keyboard
  hr = _pKeyboard->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), _keyData, &key_elements, 0);
  if(hr != DI_OK)
  {
    hr = _pKeyboard->Acquire();
    if(hr == DI_OK)
      _pKeyboard->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), _keyData, &key_elements, 0);
    else
      retval = -1;
  }

  //joysticks
  for(unsigned long i = 0; i < _pJoysticks.size(); i++) {
    joy_elements[i] = _buffer_size;
    current_joy_element[i] = 0;
    hr = _pJoysticks[i]->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), _joyData[i], &(joy_elements[i]), 0);
    if(hr != DI_OK)
    {
      hr = _pJoysticks[i]->Acquire();
      if(hr == DI_OK)
        _pJoysticks[i]->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), _joyData[i], &(joy_elements[i]), 0);
      else
        retval = -1;
    }
  }

  return retval;
}

InputData NextInput() {
  bool set = false;
  unsigned long next = 0;
  DIDEVICEOBJECTDATA * data = NULL;
  DWORD sequence;

  if(current_mouse_element < mouse_elements) {
    sequence = _mouseData[current_mouse_element].dwSequence;
    data = &(_mouseData[current_mouse_element]);
    set = true;
  }

  if(current_key_element < key_elements) {
    if(set) {
      if(DISEQUENCE_COMPARE(_keyData[current_key_element].dwSequence, <, sequence)) {
        next = 1;
        sequence = _keyData[current_key_element].dwSequence;
        data = &(_keyData[current_key_element]);
      }
    } else {
      next = 1;
      sequence = _keyData[current_key_element].dwSequence;
      data = &(_keyData[current_key_element]);
      set = true;
    }
  }

  for(unsigned long i = 0; i < _joyData.size(); i++) {
    if(current_joy_element[i] < joy_elements[i]) {
      if(set) {
        if(DISEQUENCE_COMPARE(_joyData[i][current_joy_element[i]].dwSequence, <, sequence)) {
          next = 2 + i;
          sequence = _joyData[i][current_joy_element[i]].dwSequence;
          data = &(_joyData[i][current_joy_element[i]]);
        }
      } else {
        next = 2 + i;
        sequence = _joyData[i][current_joy_element[i]].dwSequence;
        data = &(_joyData[i][current_joy_element[i]]);
        set = true;
      }
    }
  }

  if(next == 0)
    current_mouse_element++;
  else if(next == 1)
    current_key_element++;
  else
    current_joy_element[next - 2]++;

  InputData value = {next, data};

  if(!set)
    return NO_INPUT_DATA;
  else 
    return value;
}

std::string InputToString(const InputData & data) {
  std::string text;
  char temp[16];

  if(data == NO_INPUT_DATA)
    return "no input data";

  if(data.device == 0)
  {
    text = "mouse ";
    switch(data.data->dwOfs) {
    case DIMOFS_X:
      text += "x axis";
      break;
    case DIMOFS_Y:
      text += "y axis";
      break;
    case DIMOFS_Z:
      text += "z axis";
      break;
    case DIMOFS_BUTTON0:
      text += "button 0";
      break;
    case DIMOFS_BUTTON1:
      text += "button 1";
      break;
    case DIMOFS_BUTTON2:
      text += "button 2";
      break;
    default:
      text += "unknown button";
    }
  }
  else if(data.device == 1)
  {
    text = "key ";
    switch(data.data->dwOfs)
	  {
	  case DIK_ESCAPE:
		  text += "esc ";
      break;
	  case DIK_1:
		  text += "1 ";
      break;
	  case DIK_2:
		  text += "2 ";
      break;
	  case DIK_3:
		  text += "3 ";
      break;
	  case DIK_4:
		  text += "4 ";
      break;
	  case DIK_5:
		  text += "5 ";
      break;
	  case DIK_6:
		  text += "6 ";
      break;
	  case DIK_7:
		  text += "7 ";
      break;
	  case DIK_8:
		  text += "8 ";
      break;
	  case DIK_9:
		  text += "9 ";
      break;
	  case DIK_0:
		  text += "0 ";
      break;
	  case DIK_MINUS:
		  text += "- ";
      break;
	  case DIK_EQUALS:
		  text += "= ";
      break;
	  case DIK_BACK:
		  text += "backspace ";
      break;
	  case DIK_TAB:
		  text += "tab ";
      break;
	  case DIK_Q:
		  text += "Q ";
      break;
	  case DIK_W:
		  text += "W ";
      break;
	  case DIK_E:
		  text += "E ";
      break;
	  case DIK_R:
		  text += "R ";
      break;
	  case DIK_T:
		  text += "T ";
      break;
	  case DIK_Y:
		  text += "Y ";
      break;
	  case DIK_U:
		  text += "U ";
      break;
	  case DIK_I:
		  text += "I ";
      break;
	  case DIK_O:
		  text += "O ";
      break;
	  case DIK_P:
		  text += "P ";
      break;
	  case DIK_LBRACKET:
		  text += "[ ";
      break;
	  case DIK_RBRACKET:
		  text += "] ";
      break;
	  case DIK_RETURN:
		  text += "return ";
      break;
	  case DIK_A:
		  text += "A ";
      break;
	  case DIK_S:
		  text += "S ";
      break;
	  case DIK_D:
		  text += "D ";
      break;
	  case DIK_F:
		  text += "F ";
      break;
	  case DIK_G:
		  text += "G ";
      break;
	  case DIK_H:
		  text += "H ";
      break;
	  case DIK_J:
		  text += "J ";
      break;
	  case DIK_K:
		  text += "K ";
      break;
	  case DIK_L:
		  text += "L ";
      break;
	  case DIK_SEMICOLON:
		  text += "; ";
      break;
	  case DIK_APOSTROPHE:
		  text += "\' ";
      break;
	  case DIK_GRAVE:
		  text += "` ";
      break;
	  case DIK_BACKSLASH:
		  text += "\\ ";
      break;
	  case DIK_Z:
		  text += "Z ";
      break;
	  case DIK_X:
		  text += "X ";
      break;
	  case DIK_C:
		  text += "C ";
      break;
	  case DIK_V:
		  text += "V ";
      break;
	  case DIK_B:
		  text += "B ";
      break;
	  case DIK_N:
		  text += "N ";
      break;
	  case DIK_M:
		  text += "M ";
      break;
	  case DIK_COMMA:
		  text += ", ";
      break;
	  case DIK_PERIOD:
		  text += ". ";
      break;
	  case DIK_SLASH:
		  text += "/ ";
      break;
	  case DIK_MULTIPLY:
		  text += "* ";
      break;
	  case DIK_SPACE:
		  text += "SPACE ";
      break;
	  case DIK_NUMPAD7:
		  text += "num7 ";
      break;
	  case DIK_NUMPAD8:
		  text += "num8 ";
      break;
	  case DIK_NUMPAD9:
		  text += "num9 ";
      break;
	  case DIK_SUBTRACT:
		  text += "num- ";
      break;
	  case DIK_NUMPAD4:
		  text += "num4 ";
      break;
	  case DIK_NUMPAD5:
		  text += "num5 ";
      break;
	  case DIK_NUMPAD6:
		  text += "num6 ";
      break;
	  case DIK_ADD:
		  text += "num+ ";
      break;
	  case DIK_NUMPAD1:
		  text += "num1 ";
      break;
	  case DIK_NUMPAD2:
		  text += "num2 ";
      break;
	  case DIK_NUMPAD3:
		  text += "num3 ";
      break;
	  case DIK_NUMPAD0:
		  text += "num0 ";
      break;
	  case DIK_DECIMAL:
		  text += "num. ";
      break;
	  case DIK_DIVIDE:
		  text += "num/ ";
      break;
    default:
      text += "?? ";
    }

    if(data.data->dwData)
      text += "pressed";
    else
      text += "released";
  }
  else
  {
    text = std::string("joy") + ltoa(data.device, temp, 10) + " ";
    switch(data.data->dwOfs)
    {
    case DIJOFS_X:
      text += "x axis ";
      text += std::string(" ") + ltoa(data.data->dwData, temp, 10);
      break;
    case DIJOFS_Y:
      text += "y axis ";
      text += std::string(" ") + ltoa(data.data->dwData, temp, 10);
      break;
    case DIJOFS_Z:
      text += "z axis ";
      text += std::string(" ") + ltoa(data.data->dwData, temp, 10);
      break;
    default:
      text += std::string("button ") + ltoa(data.data->dwOfs - 48, temp, 10);
      text += std::string(" ") + ltoa(data.data->dwData, temp, 10);
    }
  }

  return text;
}

bool operator==(const InputData & lhs, const InputData & rhs) {
  return lhs.data == rhs.data && lhs.device == rhs.device;
}

bool operator!=(const InputData & lhs, const InputData & rhs) {
  return lhs.data != rhs.data || lhs.device != rhs.device;
}

unsigned long JoystickCount() {
  return _pJoysticks.size();
}
